﻿/*
	Copyright 2010 Jason N. / spit.respectable@gmail.com

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU Lesser General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU Lesser General Public License for more details.

	You should have received a copy of the GNU Lesser General Public License
	along with this program.  If not, see <http://www.gnu.org/licenses/>.

*/
using System;
using System.Collections.Generic;
using System.IO;

namespace jscriptshrink
{
    class Program
    {
        static void Main(string[] args)
        {
            string filename = args[0];
            if (File.Exists(filename))
            {
                string s = File.ReadAllText(filename);

                // need to rename functions before comments are removed, so that
                // we don't rename functions marked as /*external*/
                s = RenameFunctions(s);

                // remove comments
                s = RemoveComments(s);
                s = RemoveStrings(s);
                s = RemoveSpacesBeforeAndAfter(s, "():{}+-!=;,");
                s = RenameVariables(s);

                // get rid of white space

                // comment out the following line to debug issues (new lines helps to keep it human-readable)
                s = ReplaceAll(s, "\n", " ");

                s = ReplaceAll(s, "\r", " ");
                s = ReplaceAll(s, "\t", " ");

                s = ReplaceAll(s, "true", "1");
                s = ReplaceAll(s, "false", "0");

                // now remove all extra spaces, but 
                // preserve the space that are necessary
                s = ReplaceAll(s, "function ", "function~");
                s = ReplaceAll(s, "var ", "var~");
                s = ReplaceAll(s, "new ", "new~");
                s = ReplaceAll(s, "case ", "case~");
                s = ReplaceAll(s, "return ", "return~");
                s = ReplaceAll(s, "typeof ", "typeof~");
                // remove all unnecessary spaces
                s = ReplaceAll(s, " ", "");
                // restore necessary spaces
                s = ReplaceAll(s, "function~", "function ");
                s = ReplaceAll(s, "var~", "var ");
                s = ReplaceAll(s, "new~", "new ");
                s = ReplaceAll(s, "case~", "case ");
                s = ReplaceAll(s, "return~", "return ");
                s = ReplaceAll(s, "elseif", "else if");
                s = ReplaceAll(s, "typeof~", "typeof ");
                s = AddStringsBackIn(s);

                // combine string constants now instead of having javscript combine them at run-time
                s = ReplaceAll(s, "'+'", "");
                s = ReplaceAll(s, "\"+\"", "");

                s += "function builtOn() { return '" + DateTime.Now + "'; }";
                if ( args.Length > 1 )
                    File.WriteAllText(args[1],s);
                else
                    Console.WriteLine(s);

            }
        }

        private static Dictionary<string,string> strings = new Dictionary<string,string>();
        private static string newName = "aa";

        /// <summary>
        /// 
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        private static string AddStringsBackIn(string s)
        {
            foreach (KeyValuePair<string, string> kvp in strings)
            {
                string before;
                do
                {
                    before = s;
                    s = s.Replace(kvp.Key, kvp.Value);
                } while (s != before);
            }
            return s;
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        private static string RemoveStrings(string s)
        {
            strings = new Dictionary<string, string>();

            int startSingle = s.IndexOf("'");
            int startDouble = s.IndexOf("\"");

            while (startSingle > -1 || startDouble > -1)
            {
                while( startSingle > -1 )
                {
                    if ( s[startSingle-1] != '\\')
                        break;
                    startSingle = s.IndexOf("'", startSingle+1);
                }
                while( startDouble > -1 )
                {
                    if ( s[startDouble-1] != '\\')
                        break;
                    startDouble = s.IndexOf("\"", startDouble+1);
                }

                if (startSingle == -1)
                    startSingle = s.Length;
                if (startDouble == -1)
                    startDouble = s.Length;

                int start;
                string toFind;

                if (startSingle < startDouble)
                {
                    start = startSingle;
                    toFind = "'";
                }
                else
                {
                    start = startDouble;
                    toFind = "\"";
                }

                string guid = Guid.NewGuid().ToString();
                int end = s.IndexOf(toFind, start + 1);

                if (end == -1)
                    break;

                while( s[end-1] == '\\')
                {
                    end = s.IndexOf(toFind, end + 1);
                    if (end == -1)
                        break;
                }

                if (end > start + 1)
                {
                    int length = end - start + 1;
                    string remove = s.Substring(start, length);
                    strings[guid] = remove;
                    s = s.Remove(start, length).Insert(start, guid);
                }
                startSingle = s.IndexOf("'", start+2);
                startDouble = s.IndexOf("\"", start+2);
            }
            return s;
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="s"></param>
        /// <param name="findString"></param>
        /// <returns></returns>
        private static string RemoveSpacesBeforeAndAfter(string s, string findString)
        {
            char[] find = findString.ToCharArray();

            foreach (char c in find)
            {
                string charString = c.ToString();
                s = ReplaceAll(s, " " + charString, charString);
                s = ReplaceAll(s, charString + " ", charString);
            }
            return s;
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        private static string RenameFunctions(string s)
        {
            int start = s.IndexOf("function ");
            while (start != -1)
            {
                start += "function ".Length;
                int end = s.IndexOf("(", start);

                if (end == -1) continue;
                string name = s.Substring(start, end - start);

                if (name.IndexOf("/*") != -1 || name.IndexOf("Ext") != -1)
                {
                    // there is a comment between the function name and the first (, assume
                    // this is an external function and the user doesn't want it renamed
                    Console.WriteLine("Skipping rename of external function: " + name);
                }
                else
                {
                    Console.WriteLine("Renaming function: " + name + " to " + newName);
                    s = Rename(s, name, newName);
                    newName = IncrementName(newName);
                }
                start = s.IndexOf("function ",start+1);
            }
            return s;
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="s"></param>
        /// <param name="oldName"></param>
        /// <param name="newName"></param>
        /// <returns></returns>
        private static string Rename(string s, string oldName, string newName)
        {
            // BUGBUG - make a more intelligent renamer
            // check for white space before and after, so we don't rename closeCamView when trying to
            // rename closeCam() calls
            //return ReplaceAll(s, oldName, newName);

            return ReplaceAllNames(s, oldName, newName);
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        private static string IncrementName(string name)
        {
            char[] chars = name.ToCharArray();
            int len = chars.Length - 1;
            if (chars[len] == 'z')
            {
                var prepend = ( len == 0 );
                if (len > 0)
                {
                    if (chars[len - 1] == 'z')
                    {
                        chars[len - 1] = 'a';
                        prepend = true;
                    }
                    else
                        chars[len - 1] += (char) 1;
                }
                chars[len] = 'a';
                name = ( prepend ? "a" : "" ) + new string(chars);
            }
            else
            {
                chars[len] += (char)1;
                name = new string(chars);
            }


            // skip over javascript keywords
            switch( name )
            {
                case "if":
                case "in":
                case "do":
                case "then":
                case "else":
                case "while":
                    name = IncrementName(name);
                    break;
            }

            return name;
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        private static string RenameVariables(string s)
        {
            List<string> variableNames = new List<string>();

            // add global and local/scoped variables
            int start = s.IndexOf("var ");
            while (start != -1)
            {
                start += "var ".Length;
                int end = s.IndexOf(";", start);

                // BUGBUG - this doesn't handle "var i,j=2,k;" yet

                if (end == -1) continue;
                string name = s.Substring(start, end - start).Trim();

                end = name.IndexOf("=");

                if (end != -1)
                    name = name.Substring(0, end).Trim();

                if (name.Length > 1)
                    if (!variableNames.Contains(name))
                        variableNames.Add(name);

                if ( s[end+1] == '{')
                {
                    // rename all the fields in the struct
                    int structEnd = s.IndexOf("};", end + 1);
                    if ( structEnd > -1 )
                    {
                        string temp = s.Substring(end + 2, structEnd - end - 2);
                        string[] structFields = temp.Split(',');
                        for (int i = 0; i < structFields.Length; i++)
                        {
                            string fieldName = structFields[i].Split(':')[0].Trim();
                            if (!variableNames.Contains(fieldName))
                                variableNames.Add(fieldName);
                        }
                    }
                }

                start = s.IndexOf("var ", start + 1);
            }

            // add function parameters
            start = s.IndexOf("function ");
            while (start != -1)
            {
                start = s.IndexOf("(", start);
                if ( start == -1 )
                    break;
                int end = s.IndexOf(")", start);
                if ( end == -1 )
                    break;
                string parameters = s.Substring(start+1, end-start-1);

                if (parameters.Length > 1)
                {
                    string[] vars = parameters.Split(',');

                    foreach (string var in vars)
                    {
                        string cleaned = var.Trim();
                        if ((cleaned.Length <= 1) || variableNames.Contains(cleaned)) 
                            continue;
                        if (!variableNames.Contains(cleaned))
                            variableNames.Add(cleaned);
                    }
                }

                start = s.IndexOf("function ", end);
            }


            // now do the replacements
            foreach (string name in variableNames)
            {
                switch (name)
                {
                    case "value":
                    case "layer":
                    case "console":
                    case "all":
                        // skip commonly used DOM words
                        Console.WriteLine("var skipping: [{0}]", name);
                        break;
                    default:
                        Console.WriteLine("var replace: [{0}] with [{1}]", name, newName);
                        s = Rename(s, name, newName);
                        break;
                }
                newName = IncrementName(newName);
            }

            return s;
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        private static string RemoveComments(string s)
        {
            // remove C++ style comments
            int start = s.IndexOf("//");
            while( start != -1)
            {
                if (start > 0 && s[start - 1] == ':')
                {
                    // ignore web urls like "://"
                    start = s.IndexOf("//", start + 1);
                }
                else
                {
                    int end = s.IndexOf("\n", start);
                    s = s.Substring(0, start) + s.Substring(end + 1);
                    start = s.IndexOf("//", start);
                }
            }

            // remove C style comments
            start = s.IndexOf("/*");
            while (start != -1)
            {
                int end = s.IndexOf("*/", start);
                s = s.Substring(0, start) + s.Substring(end + 2);
                start = s.IndexOf("/*");
            }

            return s;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="s"></param>
        /// <param name="find"></param>
        /// <param name="replace"></param>
        /// <returns></returns>
        private static string ReplaceAllNames(string s, string find, string replace)
        {
            int start = s.IndexOf(find);
            char c;
            while (start != -1)
            {
                bool match = true;
                // make sure we have whitespace before and after (i.e. not a match in the middle of a word
                if (start > 0)
                {
                    c = s[start - 1];
                    match = !Char.IsLetter(c) && ( c != '_');
                }
                //int end = start + replace.Length;
                int end = start + find.Length;

                if (match && end < s.Length)
                {
                    c = s[end];
                    match = !Char.IsLetter(c) && (c != '_');
                }
                if (match)
                    s = s.Remove(start, find.Length).Insert(start, replace);
                start = s.IndexOf(find, start + 1);
            }
            return s;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="s"></param>
        /// <param name="find"></param>
        /// <param name="replace"></param>
        /// <returns></returns>
        private static string ReplaceAll(string s, string find, string replace)
        {
            while (s.IndexOf(find) != -1)
                s = s.Replace(find, replace);
            return s;
        }

    }
}
